<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='/assets/css/style.css') }}">

    <title>llmStudio</title>
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- CodeMirror CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/dracula.min.css">


    <style>
        #diagramContainer {
            height: 80vh;
            border: 1px solid #dee2e6;
            border-radius: 0.25rem;
            background-color: #f8f9fa;
        }

        .node-item {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            padding: 0.5rem;
            border: 1px solid #dee2e6;
            border-radius: 0.25rem;
            cursor: grab;
            margin-bottom: 0.5rem;
            background-color: white;
            transition: all 0.2s ease;
        }

        .node-item:hover {
            transform: translateY(-2px);
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .node-shape {
            width: 20px;
            height: 20px;
            border-radius: 4px;
        }

        .toolbar {
            margin-bottom: 1rem;
        }

        .node-palette {
            width: 120px;
            border-right: 1px solid #dee2e6;
            padding: 0.5rem;
        }

        .properties-panel {
            width: 1000px;
            border-left: 1px solid #dee2e6;
            padding: 0.5rem;
            position: relative;
            height: 100%;
        }

        .properties-footer {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 1rem;
            background: white;
            border-top: 1px solid #dee2e6;
        }
    </style>
</head>
<body>
<div class="container-fluid vh-100">
    <div class="row h-100">
        <!-- Main Diagram Area -->
        <div class="col">
            <!-- Toolbar -->
            <div class="toolbar" style="margin-top: 15px;">
                <button class="btn btn-outline-secondary btn-sm me-2" onclick="save()">保存</button>
                <button class="btn btn-outline-secondary btn-sm me-2" onclick="load()">加载</button>
                <button class="btn btn-outline-secondary btn-sm me-2" onclick="exportAsImage('png')">导出PNG</button>
                <button class="btn btn-outline-secondary btn-sm me-2" onclick="exportAsImage('svg')">导出SVG</button>
            </div>

            <!-- Diagram Container -->
            <div id="diagramContainer">
                <div style="width: 100%; height: fit-content; display: flex">
                    <div id="myPaletteDiv"
                         style="width: 20%; margin-right: 2px; background: white; border-right: 1px solid #dee2e6; padding: 1rem; overflow-y: auto"></div>
                    <div id="myDiagramDiv" style="width: 80%; height: 850px"></div>
                </div>
            </div>
        </div>

        <!-- Properties Panel -->
        <div class="col-auto properties-panel">
            <h5>执行面板</h5>
            <div id="propertiesPanel"></div>
            <!--            添加输入框和提交按钮-->

        </div>
    </div>
</div>

<button hidden="hidden" type="button" id="entityModalBtn" class="btn btn-primary mb-3" data-bs-toggle="modal"
        data-bs-target="#entityExampleModal">新增
</button>
<!--更新模态框-->
<div class="modal fade" id="entityExampleModal" tabindex="-1" aria-labelledby="entityExampleModal"
     aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="updateExampleModalLabel">节点信息</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <form>
                    <div id="nodeDetailsId"></div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btsn-secondary" data-bs-dismiss="modal">关闭</button>
                <button type="button" class="btn btsn-secondary" data-bs-dismiss="modal">保存</button>
            </div>
        </div>
    </div>
</div>

<!-- GoJS -->
<!--  <script src="../release/go.js"></script>-->
<!-- CodeMirror JS -->
<script src="{{ url_for('static', filename='/assets/js/jquery-3.7.1.js') }}"></script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/python/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/javascript/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/clike/clike.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/javascript/javascript.min.js"></script>

<script type="application/javascript" src="{{ url_for('static', filename='/release/go.js') }}"/>
<!--<script type="application/javascript" src="{{ url_for('static', filename='/assets/js/codeMirror.js') }}"/>-->
<!--<script type="application/javascript" src="{{ url_for('static', filename='/assets/js/flowchart.js') }}"/>-->

<script  >
    window.addEventListener("DOMContentLoaded", function () {
        // topnav
        var topButton = document.getElementById("topnavButton");
        var topnavList = document.getElementById("topnavList");
        if (topButton && topnavList) {
            topButton.addEventListener("click", function (e) {
                 topnavList
                    .classList
                    .toggle("hidden");
                e.stopPropagation();
            });
            document.addEventListener("click", function (e) {
                console.log("click=======")
                // if the clicked element isn't the list, close the list
                if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
                    topButton.click();
                }
            });

            // set active <a> element
            var url = window
                .location
                .href
                .toLowerCase();
            var aTags = topnavList.getElementsByTagName('a');
            for (var i = 0; i < aTags.length; i++) {
                var lowerhref = aTags[i]
                    .href
                    .toLowerCase();
                if (url.startsWith(lowerhref)) {
                    aTags[i]
                        .classList
                        .add('active');
                    break;
                }
            }
        }

    });
</script>

<script id="code_mirror">
    function initCodeMirror() {
        // 初始化CodeMirror编辑器
        const editor = CodeMirror.fromTextArea(document.getElementById('code-editor'), {
            lineNumbers: true,
            theme: 'dracula',
            mode: 'python',
            indentUnit: 4,
            smartIndent: true,
            matchBrackets: true,
            autoCloseBrackets: true,
        });
        editor.setValue("# 这是 Python 代码示例\nprint('Hello, World!')");

        // 语言切换功能
        document.getElementById('language-select').addEventListener('change', (e) => {
            const modeMap = {
                'python': 'python',
                'javascript': 'javascript',
                'json': {name: 'javascript', json: true},
                'java': 'text/x-java'
            };
            editor.setOption('mode', modeMap[e.target.value]);
            var selectedLanguage = e.target.value;
            // 根据选择的语言更新编辑器的模式
            if (selectedLanguage === 'python') {
                editor.setOption('mode', 'python');
                editor.setValue("# 这是 Python 代码示例\nprint('Hello, World!')");
            } else if (selectedLanguage === 'json') {
                editor.setOption('mode', 'json');
                editor.setValue('{\n    "message": "Hello, World!"\n}');
            } else if (selectedLanguage === 'java') {
                editor.setOption('mode', 'text/x-java');
                editor.setValue('// 这是 Java 代码示例\npublic class Main {\n    public static void main(String[] args) {\n        System.out.println("Hello, World!");\n    }\n}');
            }
        });
    }

    // 获取编辑器内容
    function getEditorContent() {
        return {
            content: editor.getValue(),
            language: document.getElementById('language-select').value
        };
    }

    // 设置编辑器内容和语言
    function setEditorContent(content, language) {
        editor.setValue(content);
        document.getElementById('language-select').value = language;
        const modeMap = {
            'python': 'python',
            'javascript': 'javascript',
            'json': {name: 'javascript', json: true},
            'java': 'text/x-java'
        };
        editor.setOption('mode', modeMap[language]);
    }

    // 获取代码
    function getCode() {
        const content = getEditorContent();
        alert(`获取代码成功！\n语言：${content.language}\n内容：\n${content.content}`);
    }

    // 加载代码
    function loadCode() {
        fetch('/api/code')
            .then(response => response.json())
            .then(data => {
                setEditorContent(data.content, data.language);
                alert('代码加载成功！');
            })
            .catch(error => {
                console.error('加载代码失败:', error);
                alert('加载代码失败，请稍后重试');
            });
    }

    // 暴露函数给全局
    window.getEditorContent = getEditorContent;
    window.setEditorContent = setEditorContent;
</script>

<script id="code">
    const project_id = "{{project_id}}"

    function init() {
        myDiagram = new go.Diagram('myDiagramDiv', {
            'undoManager.isEnabled': true, // enable undo & redo
            'themeManager.changesDivBackground': true,
            'themeManager.currentTheme': "white",
            'grid.visible': true,
            'grid.gridCellSize': new go.Size(10, 10),
            // 'grid.snapToGrid': true,
            'layout': new go.LayeredDigraphLayout({
                direction: 90,
                layerSpacing: 60,
                columnSpacing: 40,
                setsPortSpots: true
            }),
            'commandHandler.zoomFactor': 1.2,
            'toolManager.mouseWheelBehavior': go.ToolManager.WheelZoom
        });

        // Add zoom controls to toolbar
        const toolbar = document.querySelector('.toolbar');
        toolbar.innerHTML += `
        <button class="btn btn-outline-secondary btn-sm me-2" onclick="zoomIn()">放大</button>
        <button class="btn btn-outline-secondary btn-sm me-2" onclick="zoomOut()">缩小</button>
        <button class="btn btn-outline-secondary btn-sm me-2" onclick="zoomToFit()">适应窗口</button>
        <button class="btn btn-outline-secondary btn-sm me-2" onclick="executeFlow()">执行</button>
      `;

        // 加载事件，更新事件
        myDiagram.addDiagramListener('Modified', (e) => {
            console.log("Modified=====")
            const button = document.getElementById('SaveButton');
            if (button) button.disabled = !myDiagram.isModified;
            const idx = document.title.indexOf('*');
            if (myDiagram.isModified) {
                if (idx < 0) document.title += '*';
            } else {
                if (idx >= 0) document.title = document.title.slice(0, idx);
            }
        });

        // 添加鼠标点击事件监听器
        myDiagram.addDiagramListener("ObjectSingleClicked", function (e) {
            const part = e.subject.part;
            if (part instanceof go.Node) {
                console.log(part)
                updatePropertiesPanel(part.data);
                document.getElementById("entityModalBtn").click();
                console.log("Clicked on node: " + part.data.key);
            } else if (part instanceof go.Link) {
                console.log("Clicked on link");
            } else {
                console.log("Clicked on the diagram background");
            }
        });
        // set up some colors/fonts for the default ('light') and dark Themes
        myDiagram.themeManager.set('light', {
            colors: {
                text: '#fff',
                start: '#064e3b',
                step: '#49939e',
                conditional: '#6a9a8a',
                end: '#7f1d1d',
                comment: '#a691cc',
                bgText: '#000',
                link: '#dcb263',
                linkOver: '#cbd5e1',
                div: '#ede9e0'
            }
        });
        myDiagram.themeManager.set('dark', {
            colors: {
                text: '#fff',
                step: '#414a8d',
                conditional: '#88afa2',
                comment: '#bfb674',
                bgText: '#fff',
                link: '#fdb71c',
                linkOver: '#475569',
                div: '#141e37'
            }
        });

        defineFigures();

        // helper definitions for node templates
        function nodeStyle(node) {
            node
                // the Node.location is at the center of each node
                .set({locationSpot: go.Spot.Center})
                // The Node.location comes from the "loc" property of the node data,
                // converted by the Point.parse static method.
                // If the Node.location is changed, it updates the "loc" property of the node data,
                // converting back using the Point.stringify static method.
                .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify);
        }

        function shapeStyle(shape) {
            // make the whole node shape a port
            shape.set({strokeWidth: 0, portId: '', cursor: 'pointer'});
        }

        function textStyle(textblock) {
            textblock.set({font: 'bold 11pt Figtree, sans-serif'}).theme('stroke', 'text');
        }

        // define the Node templates for regular nodes
        // 定义节点状态颜色
        const statusColors = {
            idle: '#dbeafe',
            running: '#ff0000',  // 改为红色
            success: '#86efac',
            failed: '#fca5a5'
        };

        // 节点点击处理
        function handleNodeClick(node) {
            const nodeId = node.data.key;
            // 调用后台API
            fetch(`/api/run_node/${nodeId}`, {
                method: 'POST'
            })
                .then(response => response.json())
                .then(data => {
                    // 更新节点状态
                    myDiagram.model.commit(m => {
                        const nodeData = m.findNodeDataForKey(nodeId);
                        if (nodeData) {
                            m.set(nodeData, 'status', data.status);
                        }
                    });
                    // 启动状态轮询
                    startStatusPolling(nodeId);
                });
        }

        // 状态轮询
        function startStatusPolling(nodeId) {
            const interval = setInterval(() => {
                fetch(`/api/get_status/${nodeId}`)
                    .then(response => response.json())
                    .then(data => {
                        myDiagram.model.commit(m => {
                            const nodeData = m.findNodeDataForKey(nodeId);
                            if (nodeData) {
                                const oldStatus = nodeData.status;
                                m.set(nodeData, 'status', data.status);
                                // 如果状态变为最终状态，停止轮询
                                if (data.status === 'success' || data.status === 'failed') {
                                    clearInterval(interval);
                                }
                            }
                        });
                    });
            }, 1000); // 每秒轮询一次
        }

        myDiagram.nodeTemplateMap.add(
            '', // the default category
            new go.Node('Auto').apply(nodeStyle)
                .add(
                    new go.Shape('Rectangle', {
                        portId: 'main',
                        fromLinkable: true,
                        toLinkable: true,
                        fromSpot: go.Spot.AllSides,
                        toSpot: go.Spot.AllSides,
                        strokeWidth: 2,
                        stroke: '#2563eb',
                        fill: '#dbeafe', // 默认颜色
                        corner: 6
                    }).bind('fill', 'status', status => statusColors[status] || '#dbeafe'),
                    new go.Panel('Vertical', {margin: 8})
                        .add(
                            new go.Picture({
                                source: '',
                                width: 40,
                                height: 40,
                                margin: new go.Margin(0, 0, 4, 0)
                            }),
                            new go.TextBlock({
                                maxSize: new go.Size(160, NaN),
                                wrap: go.Wrap.Fit,
                                editable: true,
                                font: 'bold 12px Figtree, sans-serif',
                                stroke: '#1e3a8a'
                            }).bindTwoWay('text')
                        )
                )
        );

        //双击执行的方法
        function handlerDC(e, obj) {
            let node = obj.part;//拿到节点的json对象，后面要拿什么值就直接.出来
            let procTaskId = node.data.key;
            let procTaskName = node.data.text;
            let description = node.data.description;
            let procTmplId = node.data.tmplId;
            alert("addNode")
        }

        myDiagram.nodeTemplateMap.add(
            'Conditional',
            new go.Node('Auto').apply(nodeStyle)
                .add(
                    new go.Shape('Diamond', {
                        portId: 'main',
                        fromLinkable: true,
                        toLinkable: true,
                        fromSpot: go.Spot.AllSides,
                        toSpot: go.Spot.AllSides,
                        strokeWidth: 2,
                        stroke: '#d97706',
                        fill: '#fef3c7', // 默认颜色
                        minSize: new go.Size(10, 10) // Minimum size
                    }).bind('fill', 'status', status => statusColors[status] || '#fef3c7'),
                    new go.Panel('Vertical', {margin: 3})
                        .add(
                            new go.Picture({
                                source: '',
                                width: 20,
                                height: 20,
                                margin: new go.Margin(0, 0, 4, 0)
                            }),
                            new go.TextBlock({
                                maxSize: new go.Size(200, NaN),
                                wrap: go.Wrap.Fit,
                                editable: true,
                                font: 'bold 18px Figtree, sans-serif',
                                stroke: '#92400e',
                                textAlign: 'center',
                                margin: 2,
                            })
                                .bindTwoWay('text')
                        )
                )
        );

        myDiagram.nodeTemplateMap.add(
            'Start',
            new go.Node('Auto')
                .apply(nodeStyle)
                .add(
                    new go.Shape('Circle', {
                        portId: 'output',
                        fromSpot: go.Spot.Right,
                        fromLinkable: true,
                        toLinkable: true,
                        strokeWidth: 2,
                        stroke: '#059669',
                        fill: '#d1fae5',
                        width: 60,
                        height: 60
                    }),
                    new go.Panel('Vertical', {margin: 8})
                        .add(
                            new go.Picture({
                                source: '',
                                width: 24,
                                height: 24,
                                margin: new go.Margin(0, 0, 4, 0)
                            }),
                            new go.TextBlock({
                                text: '开始',
                                maxSize: new go.Size(160, NaN),
                                wrap: go.Wrap.Fit,
                                editable: true,
                                font: 'bold 12px Figtree, sans-serif',
                                stroke: '#065f46',
                                textAlign: 'center'
                            })
                        )
                )
        );

        myDiagram.nodeTemplateMap.add(
            'End',
            new go.Node('Auto')
                .apply(nodeStyle)
                .add(
                    new go.Shape('Circle', {
                        portId: 'input',
                        toSpot: go.Spot.Top,
                        strokeWidth: 2,
                        stroke: '#dc2626',
                        fill: '#fee2e2',
                        width: 60,
                        height: 60
                    }),
                    new go.Panel('Vertical', {margin: 8})
                        .add(
                            new go.Picture({
                                source: '',
                                width: 24,
                                height: 24,
                                margin: new go.Margin(0, 0, 4, 0)
                            }),
                            new go.TextBlock({
                                text: '结束',
                                maxSize: new go.Size(160, NaN),
                                wrap: go.Wrap.Fit,
                                editable: true,
                                font: 'bold 12px Figtree, sans-serif',
                                stroke: '#991b1b',
                                textAlign: 'center'
                            })
                        )
                )
        );

        myDiagram.nodeTemplateMap.add(
            'Comment',
            new go.Node('Auto').apply(nodeStyle)
                .add(
                    new go.Shape('File', {strokeWidth: 3}).theme('fill', 'div').theme('stroke', 'comment'),
                    new go.TextBlock({
                        font: '9pt Figtree, sans-serif',
                        margin: 8,
                        maxSize: new go.Size(200, NaN),
                        wrap: go.Wrap.Fit,
                        textAlign: 'center',
                        editable: true
                    })
                        .theme('stroke', 'bgText')
                        .bindTwoWay('text')
                    // no ports, because no links are allowed to connect with a comment
                )
        );

        // LLM Node Template
        myDiagram.nodeTemplateMap.add(
            'LLM',
            new go.Node('Auto').apply(nodeStyle)
                .add(
                    new go.Shape('RoundedRectangle', {
                        portId: 'main',
                        fromLinkable: true,
                        toLinkable: true,
                        fromSpot: go.Spot.Right,
                        toSpot: go.Spot.Left,
                        strokeWidth: 2,
                        stroke: '#9333ea',
                        fill: '#f3e8ff',
                        corner: 12,
                        shadowBlur: 8,
                        shadowColor: '#9333ea',
                        shadowOffset: new go.Point(2, 2)
                    }),
                    new go.Picture({
                        source: '',
                        width: 50,
                        height: 50,
                        margin: new go.Margin(0, 0, 4, 0)
                    }),
                    new go.TextBlock({
                        maxSize: new go.Size(160, NaN),
                        wrap: go.Wrap.Fit,
                        editable: true,
                        font: 'bold 12px Figtree, sans-serif',
                        stroke: '#6b21a8'
                    })
                        .bindTwoWay('text')
                )
        );

        // replace the default Link template in the linkTemplateMap
        myDiagram.linkTemplate = new go.Link({
            routing: go.Routing.Orthogonal,
            curve: go.Curve.JumpOver,
            corner: 10,
            toShortLength: 8,
            relinkableFrom: true,
            relinkableTo: true,
            reshapable: true,
            resegmentable: true,
            selectionAdorned: false,
            selectionStrokeWidth: 3,
            shadowVisible: true,
            shadowBlur: 5,
            shadowOffset: new go.Point(2, 2),
            shadowColor: 'rgba(0,0,0,0.2)',
            // Explicit port configuration
            fromPortId: 'output',
            toPortId: 'input',
            fromSpot: go.Spot.Right,
            toSpot: go.Spot.Left,
            // mouse-overs subtly highlight links:
            mouseEnter: (e, link) => (link.findObject('HIGHLIGHT').stroke = link.diagram.themeManager.findValue('linkOver', 'colors')),
            mouseLeave: (e, link) => (link.findObject('HIGHLIGHT').stroke = 'transparent'),
            // context-click creates an editable link label
            contextClick: (e, link) => {
                e.diagram.model.commit((m) => {
                    m.set(link.data, 'text', 'Label');
                });
            }
        })
            .bindTwoWay('points')
            .add(
                // the highlight shape, normally transparent
                new go.Shape({
                    isPanelMain: true,
                    strokeWidth: 8,
                    stroke: 'transparent',
                    name: 'HIGHLIGHT'
                }),
                // the link path shape
                new go.Shape({isPanelMain: true, strokeWidth: 3})
                    .theme('stroke', 'link'),
                // the arrowhead
                new go.Shape({toArrow: 'Triangle', strokeWidth: 0, scale: 2})
                    .theme('fill', 'link'),
                // the link label
                new go.Panel('Auto', {visible: false})
                    .bind('visible', 'text', (t) => typeof t === 'string' && t.length > 0) // only shown if there is text
                    .add(
                        // a gradient that fades into the background
                        new go.Shape('Ellipse', {strokeWidth: 0})
                            .theme('fill', 'div', null, null, c => new go.Brush("Radial", {0: c, 0.5: `${c}00`})),
                        new go.TextBlock({
                            name: 'LABEL',
                            font: '9pt Figtree, sans-serif',
                            margin: 3,
                            editable: true
                        })
                            .theme('stroke', 'bgText')
                            .bindTwoWay('text')
                    )
            );

        // temporary links used by LinkingTool and RelinkingTool are also orthogonal:
        myDiagram.toolManager.linkingTool.temporaryLink.routing = go.Routing.Orthogonal;
        myDiagram.toolManager.relinkingTool.temporaryLink.routing = go.Routing.Orthogonal;

        load(); // load an initial diagram from some JSON text

        // initialize the Palette that is on the left side of the page
        myPalette = new go.Palette('myPaletteDiv', {
            nodeTemplateMap: myDiagram.nodeTemplateMap, // share the templates used by myDiagram
            themeManager: myDiagram.themeManager, // share the ThemeManager used by myDiagram
            model: new go.GraphLinksModel([
                // specify the contents of the Palette
                {category: 'Start'},
                {category: 'Step', text: 'Step'},
                {category: 'LLM', text: 'LLM', key: 'llm', loc: '0 0'},
                {category: 'Conditional'},
                {category: 'End'}
            ]),
            layout: new go.GridLayout({
                wrappingColumn: 1,
                alignment: go.GridLayout.Position,
                cellSize: new go.Size(1, 1)
            })
        });
    } // end init

    // define some custom shapes for node templates
    function defineFigures() {
        go.Shape.defineFigureGenerator('Conditional', (shape, w, h) => {
            const geo = new go.Geometry();
            const fig = new go.PathFigure(w * 0.15, 0, true);
            geo.add(fig);
            fig.add(new go.PathSegment(go.SegmentType.Line, w * 0.85, 0));
            fig.add(new go.PathSegment(go.SegmentType.Line, w, 0.5 * h));
            fig.add(new go.PathSegment(go.SegmentType.Line, w * 0.85, h));
            fig.add(new go.PathSegment(go.SegmentType.Line, w * 0.15, h));
            fig.add(new go.PathSegment(go.SegmentType.Line, 0, 0.5 * h).close());
            geo.spot1 = new go.Spot(0.15, 0);
            geo.spot2 = new go.Spot(0.85, 1);
            return geo;
        });

        // taken from ../extensions/Figures.js:
        go.Shape.defineFigureGenerator('File', (shape, w, h) => {
            const geo = new go.Geometry();
            const fig = new go.PathFigure(0, 0, true); // starting point
            geo.add(fig);
            fig.add(new go.PathSegment(go.SegmentType.Line, 0.75 * w, 0));
            fig.add(new go.PathSegment(go.SegmentType.Line, w, 0.25 * h));
            fig.add(new go.PathSegment(go.SegmentType.Line, w, h));
            fig.add(new go.PathSegment(go.SegmentType.Line, 0, h).close());
            const fig2 = new go.PathFigure(0.75 * w, 0, false);
            geo.add(fig2);
            // The Fold
            fig2.add(new go.PathSegment(go.SegmentType.Line, 0.75 * w, 0.25 * h));
            fig2.add(new go.PathSegment(go.SegmentType.Line, w, 0.25 * h));
            geo.spot1 = new go.Spot(0, 0.25);
            geo.spot2 = go.Spot.BottomRight;
            return geo;
        });
    }

    // Show the diagram's model in JSON format that the user may edit
    function save() {
        var jsonString = document.getElementById('mySavedModel').value = myDiagram.model.toJson();
        const jsonObject = JSON.parse(jsonString);
        var nodeDatas = jsonObject.nodeDataArray;
        var linkDatas = jsonObject.linkDataArray;
        var data = {
            "node_data": nodeDatas,
            "link_data": linkDatas,
            "project_id": project_id
        };
        commonPostXhr("/flowchart/nodes_links", data);
        console.log(data)
        myDiagram.isModified = false;
    }

    function load() {
        fetch(`/flowchart/data/${project_id}`)
            .then(response => response.json())
            .then(data => {
                console.log(data.node_data)
                console.log(data.link_data)
                myDiagram.model = new go.GraphLinksModel(
                    {
                        linkFromPortIdProperty: "fromPort",  // required information:
                        linkToPortIdProperty: "toPort",
                        copiesArrays: true,
                        copiesArrayObjects: true,
                        nodeDataArray: data.node_data,
                        linkDataArray: data.link_data
                    });
            })

        //  nodeData = [
        //     {"key": -1, "category": "Start", "loc": "-114.6640625 -55.859375", "text": "Start"},
        //     {"category": "Step", "text": "load_data", "key": -8, "loc": "-113.78125 35.76953125"},
        //     {"category": "Step", "text": "preprocess", "key": -9, "loc": "-114.1484375 138"},
        //     {
        //         "category": "Step",
        //         "text": "generate_factor",
        //         "key": -19,
        //         "loc": "-113.65931701660162 246.56851599937264"
        //     },
        //     {"category": "Step", "text": "backtest", "key": -20, "loc": "-285.5861919581911 420.60543050822275"},
        //     {"category": "End", "text": "End", "key": -13, "loc": "-93.10847743273929 682.3625880053542"},
        //     {"category": "Conditional", "text": "evaluate", "key": -10, "loc": "-91.56958548228704 523.8707818169996"}
        // ];
        // var linkData = [
        //     {"from": -1, "to": -8, "text": ""},
        //     {"from": -8, "to": -9, "text": ""},
        //     {"from": -9, "to": -19, "text": ""},
        //     {"from": -19, "to": -20, "text": ""},
        //     {"from": -10, "to": -13, "text": "Yes"},
        //     {"from": -10, "to": -19, "text": "NO"},
        //     {"from": -20, "to": -10, "text": ""}
        // ];
        // myDiagram.model = go.Model.fromJson(document.getElementById('mySavedModel').value);

    }

    // Zoom controls
    function zoomIn() {
        myDiagram.commandHandler.increaseZoom();
    }

    function zoomOut() {
        myDiagram.commandHandler.decreaseZoom();
    }

    function zoomToFit() {
        myDiagram.commandHandler.zoomToFit();
    }

    // Update properties panel
    function updatePropertiesPanel(data) {
        const panel = document.getElementById('nodeDetailsId');
        console.log(data)
        panel.innerHTML = `
        <div class="md:mb-6">
          <label class="form-label">节点名称</label>
          <input type="text" disabled class="form-control" value="${data.text || ''}" onchange="updateNodeText(${data.key}, this.value)">
        </div>
        <div class="md:mb-6">
          <label class="form-label">节点类型</label>
          <input type="text" disabled class="form-control" value="${data.category || ''}" onchange="updateNodeText(${data.key}, this.value)">
        </div>
        <div class="md:mb-6">
          <div class="card">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h5 class="mb-0">代码编辑器</h5>
                    <div class="btn-group">
<!--                        <button type="button" class="btn btn-sm btn-outline-primary" onclick="getCode()">-->
<!--                            <i class="bi bi-download"></i> 获取代码-->
<!--                        </button>-->
<!--                        <button type="button" class="btn btn-sm btn-outline-success" onclick="loadCode()">-->
<!--                            <i class="bi bi-upload"></i> 加载代码-->
<!--                        </button>-->
                        <select id="language-select" class="form-select form-select-sm">
                            <option value="python">Python</option>
                            <option value="javascript">JavaScript</option>
                            <option value="json">JSON</option>
                            <option value="java">Java</option>
                        </select>
                    </div>
                </div>
                <div class="card-body p-0">
                    <textarea id="code-editor"></textarea>
                </div>
            </div>
      `;

        initCodeMirror();
    }

    // 处理提交按钮点击事件
    function submitComment(key) {
        const comment = document.getElementById('nodeComment').value;
        myDiagram.model.commit((m) => {
            const nodeData = m.findNodeDataForKey(key);
            if (nodeData) {
                m.set(nodeData, 'comment', comment);
                alert('备注已保存');
            }
        });
    }

    function updateNodeText(key, text) {
        myDiagram.model.commit((m) => {
            const nodeData = m.findNodeDataForKey(key);
            if (nodeData) {
                m.set(nodeData, 'text', text);
            }
        });
    }

    function updateNodeColor(key, color) {
        myDiagram.model.commit((m) => {
            const nodeData = m.findNodeDataForKey(key);
            if (nodeData) {
                m.set(nodeData, 'color', color);
                const node = myDiagram.findNodeForKey(key);
                if (node) {
                    node.findObject('SHAPE').fill = color;
                }
            }
        });
    }

    // print the diagram by opening a new window holding SVG images of the diagram contents for each page
    function exportAsImage(format) {
        const diagram = myDiagram;
        const scale = 2; // Higher resolution
        const bounds = diagram.documentBounds;

        if (format === 'png') {
            const img = diagram.makeImage({
                scale: scale,
                size: bounds.size,
                background: '#ffffff'
            });

            const link = document.createElement('a');
            link.download = 'diagram.png';
            link.href = img.src;
            link.click();
        } else if (format === 'svg') {
            const svg = diagram.makeSvg({
                scale: scale,
                size: bounds.size,
                background: '#ffffff'
            });

            const serializer = new XMLSerializer();
            const source = serializer.serializeToString(svg);

            const link = document.createElement('a');
            link.download = 'diagram.svg';
            link.href = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source);
            link.click();
        }
    }

    function changeTheme() {
        const myDiagram = go.Diagram.fromDiv('myDiagramDiv');
        if (myDiagram) {
            myDiagram.themeManager.currentTheme = document.getElementById('theme').value;
        }
    }

    window.addEventListener('DOMContentLoaded', () => {
        // setTimeout only to ensure font is loaded before loading diagram
        // you may want to use an asset loading library for this
        // to keep this sample simple, it does not
        setTimeout(() => {
            init();
        }, 300);
    });

    // 执行流程图
    async function executeFlow() {
        const nodes = myDiagram.model.nodeDataArray;
        const startNode = nodes.find(n => n.category === 'Start');
        if (!startNode) {
            alert('请添加开始节点');
            return;
        }

        // 重置所有节点状态
        myDiagram.model.commit(m => {
            nodes.forEach(n => m.set(n, 'status', 'idle'));
        });

        // 从开始节点执行
        await executeNode(startNode);

        async function executeNode(node) {

            console.log(node)
            // 设置当前节点为运行中
            myDiagram.model.commit(m => {
                m.set(node, 'status', 'running');
            });
            // 模拟节点执行
            await new Promise(resolve => setTimeout(resolve, 1000));

            // 设置当前节点为完成
            myDiagram.model.commit(m => {
                m.set(node, 'status', 'success');
            });

            // 获取下一个节点
            const nextNodes = myDiagram.model.linkDataArray
                .filter(l => l.from === node.key)
                .map(l => nodes.find(n => n.key === l.to));

            // 递归执行后续节点
            for (const nextNode of nextNodes) {
                if (nextNode) {
                    await executeNode(nextNode);
                }
            }
        }
    }

    function commonPostXhr(url, value) {
        let xhr = new XMLHttpRequest();
        xhr.open('POST', url, true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify(value));
        xhr.onload = function () {
            if (xhr.status === 200) {
                console.log(xhr.responseText);
                window.location.reload();
            } else {
                console.log('Error: ' + xhr.status);
            }
        };
    }
</script>
</body>
<!-- This script is part of the gojs.net website, and is not needed to run the sample
